package top.fullj.bean;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;

/**
 * @author bruce.wu
 * @since 2021/6/9 15:01
 */
@SuppressWarnings({"unused", "WeakerAccess"})
public final class ToString {

    private static final Collection<Class<?>> COMPAT_TYPES = new HashSet<>();

    private static final Cache<Class<?>, Map<String, Field>> TYPE_FIELDS_CACHE = Caffeine.newBuilder()
            .maximumSize(8)
            .build();

    private static final char PACKAGE_SEPARATOR_CHAR = '.';

    public static void registerCompatTypes(Class<?>...types) {
        COMPAT_TYPES.addAll(Arrays.asList(types));
    }

    public static MoreObjects.ToStringHelper toStringHelper(Object self) {
        return MoreObjects.toStringHelper(self)
                .omitNullValues();
    }

    public static String maskSecret(String secret) {
        return mask(secret, "****", "****", "");
    }

    public static String mask(String str, String start, String mask, String end) {
        if (Strings.isNullOrEmpty(str)) {
            return str;
        }
        int startLen = start.length();
        if (str.length() <= startLen) {
            return start + mask + end;
        }
        int endLen = end.length();
        if (str.length() < (startLen + endLen)) {
            return str.substring(0, startLen) + mask + end;
        }
        return str.substring(0, startLen) + mask + str.substring(str.length() - endLen);
    }

    public static String simpleClassName(Class<?> type) {
        String className = type.getName();
        final int lastDotIdx = className.lastIndexOf(PACKAGE_SEPARATOR_CHAR);
        if (lastDotIdx > -1) {
            return className.substring(lastDotIdx + 1);
        }
        return className;
    }

    public static String toStringCompat(Object pojo) {
        if (pojo == null) return "null";
        Class<?> type = pojo.getClass();
        if (type.isArray()) {
            int size = Array.getLength(pojo);
            StringBuilder sb = new StringBuilder("[");
            for (int i = 0; i < size; i++) {
                if (i > 0) sb.append(",");
                sb.append(toStringCompat(Array.get(pojo, i)));
            }
            sb.append("]");
            return sb.toString();
        }
        if (Map.class.isAssignableFrom(type)) {
            StringBuilder sb = new StringBuilder("[");
            for (Map.Entry<?,?> entry : ((Map<?, ?>) pojo).entrySet()) {
                sb.append(",").append(entry.getKey()).append("=").append(toStringCompat(entry.getValue()));
            }
            sb.append("]");
            if (sb.length() > 2) sb.deleteCharAt(1);
            return sb.toString();
        }
        if (Collection.class.isAssignableFrom(type)) {
            StringBuilder sb = new StringBuilder("[");
            for (Object item : (Collection<?>) pojo) {
                sb.append(",").append(toStringCompat(item));
            }
            sb.append("]");
            if (sb.length() > 2) sb.deleteCharAt(1);
            return sb.toString();
        }
        if (!COMPAT_TYPES.contains(type)) {
            return pojo.toString();
        }
        Map<String, Field> fields = TYPE_FIELDS_CACHE.get(type, BeanUtils::getFields);
        assert fields != null;
        MoreObjects.ToStringHelper helper = toStringHelper(pojo);
        for (Map.Entry<String, Field> entry : fields.entrySet()) {
            String name = entry.getKey();
            Field field = entry.getValue();
            boolean ignored = field.isAnnotationPresent(Ignore.class)
                    || Modifier.isTransient(field.getModifiers())
                    || Modifier.isStatic(field.getModifiers());
            if (ignored) continue;
            try {
                field.setAccessible(true);
                if (field.isAnnotationPresent(Masked.class)
                        && String.class == field.getType()) {
                    Masked masked = field.getAnnotation(Masked.class);
                    String value = (String)field.get(pojo);
                    helper.add(name, ToString.mask(value, masked.start(), masked.mask(), masked.end()));
                } else {
                    helper.add(name, toStringCompat(field.get(pojo)));
                }
            } catch (ReflectiveOperationException ignore) {

            }
        }
        return helper.toString();
    }

    private final Object pojo;

    public ToString(Object pojo) {
        this.pojo = pojo;
    }

    @Override
    public String toString() {
        return toStringCompat(pojo);
    }

}
